<?php

namespace helper\union;

class Pay
{
    private array $config;

    public function __construct($config)
    {
        $this->config = [
            'mer_id'           => $config['mer_id'],
            'notify_url'       => $config['notify_url'] ?? '',
            'sign_cert_path'   => $config['sign_cert_path'],
            'sign_cert_pwd'    => $config['sign_cert_pwd'],
            'middle_cert_path' => $config['middle_cert_path'],
            'root_cert_path'   => $config['root_cert_path'],
            'ifValidateCNName' => false
        ];
    }

    /**
     * page 支付
     * 统一收单下单并支付页面接口
     * @param string $subject 订单标题
     * @param string $out_trade_no 商户订单号
     * @param float $total_amount 订单总金额
     * @param string $return_url 页码返回支付结果
     * @return string
     */
    public function page(string $subject, string $out_trade_no, float $total_amount, string $return_url)
    {
        $params = array(

            //以下信息非特殊情况不需要改动
            'version'      => '5.1.0',                 //版本号
            'encoding'     => 'utf-8',                  //编码方式
            'txnType'      => '01',                      //交易类型
            'txnSubType'   => '01',                  //交易子类
            'bizType'      => '000201',                  //业务类型
            'frontUrl'     => $return_url,           //前台通知地址
            'backUrl'      => $this->config['notify_url'],//后台通知地址
            'signMethod'   => '01',                  //签名方法
            'channelType'  => '08',                  //渠道类型，07-PC，08-手机
            'accessType'   => '0',                  //接入类型
            'currencyCode' => '156',              //交易币种，境内商户固定156

            //TODO 以下信息需要填写
            'merId'        => $this->config['mer_id'],   //商户代码，请改自己的测试商户号，此处默认取demo演示页面传递的参数
            'orderId'      => $out_trade_no,    //商户订单号，8-32位数字字母，不能含“-”或“_”，此处默认取demo演示页面传递的参数，可以自行定制规则
            'txnTime'      => date('YmdHis'),    //订单发送时间，格式为YYYYMMDDhhmmss，取北京时间，此处默认取demo演示页面传递的参数
            'txnAmt'       => $total_amount * 100,    //交易金额，单位分，此处默认取demo演示页面传递的参数

            // 订单超时时间。
            // 超过此时间后，除网银交易外，其他交易银联系统会拒绝受理，提示超时。 跳转银行网银交易如果超时后交易成功，会自动退款，大约5个工作日金额返还到持卡人账户。
            // 此时间建议取支付时的北京时间加15分钟。
            // 超过超时时间调查询接口应答origRespCode不是A6或者00的就可以判断为失败。
            'payTimeout'   => date('YmdHis', strtotime('+15 minutes')),

            'riskRateInfo' => "{commodityName=$subject}",

            // 请求方保留域，
            // 透传字段，查询、通知、对账文件中均会原样出现，如有需要请启用并修改自己希望透传的数据。
            // 出现部分特殊字符时可能影响解析，请按下面建议的方式填写：
            // 1. 如果能确定内容不会出现&={}[]"'等符号时，可以直接填写数据，建议的方法如下。
            //    'reqReserved' =>'透传信息1|透传信息2|透传信息3',
            // 2. 内容可能出现&={}[]"'符号时：
            // 1) 如果需要对账文件里能显示，可将字符替换成全角＆＝｛｝【】“‘字符（自己写代码，此处不演示）；
            // 2) 如果对账文件没有显示要求，可做一下base64（如下）。
            //    注意控制数据长度，实际传输的数据长度不能超过1024位。
            //    查询、通知等接口解析时使用base64_decode解base64后再对数据做后续解析。
            //    'reqReserved' => base64_encode('任意格式的信息都可以'),

            //TODO 其他特殊用法请查看 special_use_purchase.php
        );

        $this->sign($params);
        $uri = 'https://gateway.test.95516.com/gateway/api/frontTransReq.do';

        $encodeType = $params['encoding'] ?? 'UTF-8';
        $html       = <<<eot
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset={$encodeType}" />
</head>
<body onload="javascript:document.pay_form.submit();">
    <form id="pay_form" name="pay_form" action="{$uri}" method="post">
eot;
        foreach ($params as $key => $value) {
            $html .= "    <input type=\"hidden\" name=\"{$key}\" id=\"{$key}\" value=\"{$value}\" />\n";
        }
        $html .= <<<eot
    </form>
</body>
</html>
eot;
        return $html;
    }

    public function app(string $subject, string $out_trade_no, float $total_amount): array
    {
        $params = array(

            //以下信息非特殊情况不需要改动
            'version'      => '5.1.0',                 //版本号
            'encoding'     => 'utf-8',                  //编码方式
            'txnType'      => '01',                      //交易类型
            'txnSubType'   => '01',                  //交易子类
            'bizType'      => '000201',                  //业务类型
            'frontUrl'     => $this->config['notify_url'],  //前台通知地址
            'backUrl'      => $this->config['notify_url'],      //后台通知地址
            'signMethod'   => '01',                  //签名方法
            'channelType'  => '08',                  //渠道类型，07-PC，08-手机
            'accessType'   => '0',                  //接入类型
            'currencyCode' => '156',              //交易币种，境内商户固定156

            //TODO 以下信息需要填写
            'merId'        => 1111111111,        //商户代码，请改自己的测试商户号，此处默认取demo演示页面传递的参数
            'orderId'      => $out_trade_no,    //商户订单号，8-32位数字字母，不能含“-”或“_”，此处默认取demo演示页面传递的参数，可以自行定制规则
            'txnTime'      => date('YmdHis', time()),    //订单发送时间，格式为YYYYMMDDhhmmss，取北京时间，此处默认取demo演示页面传递的参数
            //todo 支付金额
            'txnAmt'       => $total_amount * 100,    //交易金额，单位分，此处默认取demo演示页面传递的参数

            // 请求方保留域，
            // 透传字段，查询、通知、对账文件中均会原样出现，如有需要请启用并修改自己希望透传的数据。
            // 出现部分特殊字符时可能影响解析，请按下面建议的方式填写：
            // 1. 如果能确定内容不会出现&={}[]"'等符号时，可以直接填写数据，建议的方法如下。
            //    'reqReserved' =>'透传信息1|透传信息2|透传信息3',
            // 2. 内容可能出现&={}[]"'符号时：
            // 1) 如果需要对账文件里能显示，可将字符替换成全角＆＝｛｝【】“‘字符（自己写代码，此处不演示）；
            // 2) 如果对账文件没有显示要求，可做一下base64（如下）。
            //    注意控制数据长度，实际传输的数据长度不能超过1024位。
            //    查询、通知等接口解析时使用base64_decode解base64后再对数据做后续解析。
            //    'reqReserved' => base64_encode('任意格式的信息都可以'),

            //TODO 其他特殊用法请查看 pages/api_05_app/special_use_purchase.php
        );

        AcpService::sign($params); // 签名
        $url = SDKConfig::getSDKConfig()->appTransUrl;

        $result_arr = AcpService::post($params, $url);

        if (count($result_arr) <= 0) { //没收到200应答的情况
            throw new \Exception('发起支付失败');
            //Order::saveLog('error:unionpay:order_id:'.$orderId.':user_id:'.$userId.':get order string failed,not receive response');
        }

        if (!AcpService::validate($result_arr)) {
            //Order::saveLog('error:unionpay:order_id:'.$orderId.':user_id:'.$userId.':get order string failed,response validate failed');
            throw new \Exception('发起支付失败');
        }
        if ($result_arr["respCode"] == "00") {
            return ['tn' => $result_arr["tn"]];
        } else {
            //其他应答码做以失败处理
            //Order::saveLog('error:unionpay:order_id:'.$orderId.':user_id:'.$userId.':get order string failed,'.$result_arr["respMsg"]);
            throw new \Exception('发起支付失败');
        }
    }

    public function transfer(string $out_trade_no, string $amount, string $account, string $name, string $remark): array
    {
        // TODO: Implement transfer() method.
    }

    public function verifyNotify(array $params): array
    {
        if (!isset ($params ['signature'])) {
            return [];
        }
        $signature_str = $params['signature'];
        unset ($params['signature']);
        $params_str = $this->createLinkString($params, true, false);

        $strCert = $params['signPubKeyCert'];
        $strCert = $this->verifyAndGetVerifyCert($strCert);
        if (!$strCert) {
            return [];
        }
        $params_sha256x16 = hash('sha256', $params_str);
        $signature        = base64_decode($signature_str);
        $isSuccess        = openssl_verify($params_sha256x16, $signature, $strCert, "sha256");
        if (!$isSuccess) {
            return [];
        }
        return $params;
        // orderId,respCode 判断respCode=00、A6后，对涉及资金类的交易，请再发起查询接口查询，确定交易成功后更新数据库。
    }

    private function verifyAndGetVerifyCert($certBase64String)
    {
        openssl_x509_read($certBase64String);
        $certInfo = openssl_x509_parse($certBase64String);

        $cn = $this->getIdentitiesFromCertficate($certInfo);
        $company = '中国银联股份有限公司';
        if (strtolower($this->config['ifValidateCNName']) == "true") {
            if ($company != $cn) {
                return null;
            }
        } else if ($company != $cn && "00040000:SIGN" != $cn) {
            return null;
        }

        $from      = date_create('@' . $certInfo ['validFrom_time_t']);
        $to        = date_create('@' . $certInfo ['validTo_time_t']);
        $now       = date_create(date('Ymd'));
        $interval1 = $from->diff($now);
        $interval2 = $now->diff($to);
        if ($interval1->invert || $interval2->invert) {
            return null;
        }
        $result = openssl_x509_checkpurpose($certBase64String, X509_PURPOSE_ANY, array($this->config['root_cert_path'], $this->config['middle_cert_path']));
        if ($result === TRUE) {
            return $certBase64String;
        } else {
            return null;
        }
    }

    private function getIdentitiesFromCertficate($certInfo)
    {
        $cn      = $certInfo['subject'];
        $cn      = $cn['CN'];
        $company = explode('@', $cn);
        if (count($company) < 3) {
            return null;
        }
        return $company[2];
    }

    /**
     * 签名
     * @return mixed
     */
    private function sign(&$params)
    {
        if ($params['signMethod'] == '01') {
            return $this->signByCertInfo($params, $this->config['sign_cert_path'], $this->config['sign_cert_pwd']);
        } else {
            return $this->signBySecureKey($params, $this->config['secureKey']);
        }
    }

    // 获取签名信息
    private function signByCertInfo(&$params, $cert_path, $cert_pwd)
    {
        //证书ID
        $cert             = $this->getSignCertFromPfx($cert_path, $cert_pwd);
        $params['certId'] = $cert['certId'];
        $private_key      = $cert['key'];
        // 转换成key=val&串
        $params_str = $this->createLinkString($params, true, false);
        //sha256签名摘要
        $params_sha256x16 = hash('sha256', $params_str);
        // 签名
        $result = openssl_sign($params_sha256x16, $signature, $private_key, 'sha256');
        if ($result) {
            $signature_base64     = base64_encode($signature);
            $params ['signature'] = $signature_base64;
        }
        return $result;
    }

    private function signBySecureKey(&$params, $secureKey)
    {
        if ($secureKey == null || trim($secureKey) == '') {
            return false;
        }
        if ($params['signMethod'] == '11') {
            // 转换成key=val&串
            $params_str           = $this->createLinkString($params, true, false);
            $params_before_sha256 = hash('sha256', $secureKey);
            $params_before_sha256 = $params_str . '&' . $params_before_sha256;
            $params_after_sha256  = hash('sha256', $params_before_sha256);
            $params ['signature'] = $params_after_sha256;
            $result               = true;
        } else if ($params['signMethod'] == '12') {
            $result = false;
        } else {
            $result = false;
        }
        return $result;
    }

    // 获取证书信息
    private static function getSignCertFromPfx($certPath, $certPwd)
    {
        $pkcs12certdata = file_get_contents($certPath);
        if ($pkcs12certdata === false) {
            return [];
        }
        $certs = [];
        if (!openssl_pkcs12_read($pkcs12certdata, $certs, $certPwd)) {
            return [];
        }
        $x509data = $certs['cert'];
        if (!openssl_x509_read($x509data)) {
            return [];
        }
        $certdata       = openssl_x509_parse($x509data);
        $cert['certId'] = $certdata ['serialNumber'];
        $cert['key']    = $certs ['pkey'];
        $cert['cert']   = $x509data;
        return $cert;
    }

    /**
     * 数组转换为string
     *
     * @param array $para 数组
     * @param bool $sort 是否需要排序
     * @param bool $encode 是否需要URL编码
     * @return string
     */
    private function createLinkString(array $para, bool $sort, bool $encode): string
    {
        // 对数组排序
        if ($sort) {
            ksort($para);
        }
        $linkString = "";
        foreach ($para as $key => $value) {
            if ($encode) {
                $value = urlencode($value);
            }
            $linkString .= $key . "=" . $value . "&";
        }
        // 去掉最后一个&字符
        return substr($linkString, 0, -1);
    }
}
